﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using XL_ModbusRtu.Enums;

namespace XL_ModbusRtu
{
    public class ModbusRtu
    {
        private ICommunications _comm;
        private byte _unit;
        public ModbusRtu(byte unit, ICommunications comm)
        {
            _comm = comm;
            _unit = unit;
        }

        public void Close() => _comm.Close();
        public bool Open() => _comm.Open();

        /// <summary>
        /// 发送数据与请求数据
        /// </summary>
        /// <param name="iCommand"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        private int Execute(ref byte[] bytes, int length)
        {
            byte[] buffer = _comm.Read();
            Array.Copy(buffer, 0, bytes, length, buffer.Length);
            length += buffer.Length;
            return length;
        }

        /// <summary>
        /// 检测返回的报文是否正确
        /// </summary>
        /// <param name="bytes">完整的报文</param>
        /// <returns></returns>
        private bool IsIncorrectResponse(byte[] bytes)
        {
            if (bytes.Length < 5) { return false; }
            int length = 8;
            if (bytes[1] > 0x80) { throw new Exception($"异常功能码：[{(MudbosErrorCode)bytes[8]}]"); }
            switch (bytes[1])
            {
                case 0x01:
                case 0x02:
                case 0x03:
                case 0x04:
                    length = 5 + bytes[2];
                    break;
            }
            if (bytes.Length != length) { throw new Exception("报文不完整"); }
            return false;
        }

        /// <summary>
        /// 校验报文
        /// </summary>
        /// <param name="iCommand"></param>
        /// <returns></returns>
        private byte[] TryExecution(byte[] iCommand)
        {
            byte[] rtResponse = new byte[256];
            _comm.Write(iCommand);
            int tCount = 3;
            int length = 0;
            do
            {
                length += Execute(ref rtResponse, length);
                --tCount;
                if (tCount < 0)
                {
                    throw new Exception("无法获得正确的报文!!!");
                }
            } while (IsIncorrectResponse(rtResponse.Take(length).ToArray()));
            return rtResponse.Take(length).ToArray();
        }

        /// <summary>
        /// 设置报文
        /// </summary>
        /// <param name="iMainCommand"></param>
        /// <param name="iSubCommand"></param>
        /// <param name="iData"></param>
        /// <returns></returns>
        private byte[] SetCommand(ModbusEnums iMainCommand, byte[] iData)
        {
            byte[] bytes = new byte[4 + iData.Length];
            bytes[0] = _unit;
            bytes[1] = (byte)iMainCommand;
            Array.Copy(iData, 0, bytes, 2, iData.Length);
            byte[] crc = bytes.Take(bytes.Length - 2).ToArray().GetCRC();
            Array.Copy(crc, 0, bytes, bytes.Length - 2, 2);
            return bytes;
        }


        public bool SetRegister(int iaddress, List<ushort> value)
        {
            List<byte> command = new List<byte>()
            {
                (byte)(iaddress >> 8),
                (byte)iaddress,
                0x00,
            };
            //byte[] command = new byte[]
            int length = value.Count;
            command.Add((byte)length);
            command.Add((byte)(length * 2));
            foreach (var item in value)
            {
                command.Add((byte)(item >> 8));
                command.Add((byte)item);
            }
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_MULTIPLE_REGISTER, command.ToArray());
            byte[] rtResponse = TryExecution(sdCommand);
            return rtResponse[10] == sdCommand[10] && rtResponse[11] == sdCommand[11];
        }

        public bool SetRegister(int iaddress, ushort value)
        {
            byte[] command = new byte[] 
            {
                (byte)(iaddress >> 8),
                (byte)iaddress,
                (byte)(value >> 8),
                (byte)value
            };
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            int ctLength = rtResponse.Length;
            byte[] crc = rtResponse.Take(ctLength - 2).ToArray().GetCRC();
            return rtResponse[ctLength - 2] == crc[0] && rtResponse[ctLength - 1] == crc[1];
        }

        public bool SetBitRegister(int iaddress, int offset, bool bl)
        {
            int value = bl ? 0x01 << offset : 0;
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(value >> 8);
            command[3] = (byte)value;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);

            int ctLength = rtResponse.Length;
            byte[] crc = rtResponse.Take(ctLength - 2).ToArray().GetCRC();
            return rtResponse[ctLength - 2] == crc[0] && rtResponse[ctLength - 1] == crc[1];
        }

        public bool SetBitRegister(int iaddress, ushort old_value, int offset, bool bl)
        {
            
            ushort value = (ushort)(old_value | (bl ? (0x01 << offset) : 0));
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(value >> 8);
            command[3] = (byte)value;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);

            int ctLength = rtResponse.Length;
            byte[] crc = rtResponse.Take(ctLength - 2).ToArray().GetCRC();
            return rtResponse[ctLength - 2] == crc[0] && rtResponse[ctLength - 1] == crc[1];
        }

        public bool SetBitsRegister(int iaddress, List<bool> value)
        {
            ushort values = value.BoolsToUShort();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(values >> 8);
            command[3] = (byte)values;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);

            int ctLength = rtResponse.Length;
            byte[] crc = rtResponse.Take(ctLength - 2).ToArray().GetCRC();
            return rtResponse[ctLength - 2] == crc[0] && rtResponse[ctLength - 1] == crc[1];
        }

        public bool SetRegister(int iaddress, ushort[] value)
        {
            
            int count = value.Length;
            List<byte> command = new List<byte>()
            {
                (byte)(iaddress >> 8),
                (byte)iaddress,
                (byte)(count >> 8),
                (byte)count,
                (byte)(value.Length * 2),
            };
            foreach (var item in value)
            {
                command.Add((byte)(item >> 8));
                command.Add((byte)item);
            }
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_MULTIPLE_REGISTER, command.ToArray());
            byte[] rtResponse = TryExecution(sdCommand);

            int ctLength = rtResponse.Length;
            byte[] crc = rtResponse.Take(ctLength - 2).ToArray().GetCRC();
            return rtResponse[ctLength - 2] == crc[0] && rtResponse[ctLength - 1] == crc[1];
        }

        public bool ReadBitRegister(int iaddress, int offset = 0) => ReadBitsRegister(iaddress, 16)[offset];

        public List<bool> ReadBitsRegister(int iaddress, int count)
        {
            int length = (count / 16) + ((count % 16 > 0) ? 1 : 0);
            List<bool> value = new List<bool>();
            List<ushort> list = ReadRegister(iaddress, length);
            for (int i = 0; i < list.Count; i++)
            {
                for (int j = 0; j < 16; j++)
                {
                    value.Add(((list[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        private ushort ByteToUShort(byte byte1, byte byte2) => (ushort)((byte1 << 8) + byte2);
        private short ByteToShort(byte byte1, byte byte2) => (short)((byte1 << 8) + byte2);
        private int ByteToInt(byte[] bytes) => (ByteToShort(bytes[2], bytes[3]) << 16) + ByteToShort(bytes[0], bytes[1]);
        private int ByteToUInt(byte[] bytes) => (ByteToUShort(bytes[2], bytes[3]) << 16) + ByteToUShort(bytes[0], bytes[1]);

        public List<ushort> ReadRegister(int iaddress, int count)
        {
            
            List<ushort> value = new List<ushort>();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_HOLDING_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            
            for (int i = 3; i < rtResponse.Length - 2; i = i + 2)
            {
                value.Add((ushort)((rtResponse[i] << 8) + rtResponse[i + 1]));
            }
            return value;
        }

        public ushort ReadInputRegister(int iaddress) => ReadInputRegister(iaddress, 1)[0];
        public List<ushort> ReadInputRegister(int iaddress, int count)
        {
            
            List<ushort> list = new List<ushort>();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_INPUT_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            
            for (int i = 3; i < rtResponse.Length - 2; i = i + 2)
            {
                list.Add((ushort)((rtResponse[i] << 8) + rtResponse[i + 1]));
            }
            return list;
        }
        public bool ReadInputBitRegister(int iaddress, int offset = 0) => ReadInputBitsRegister(iaddress, 16)[offset];

        public List<bool> ReadInputBitsRegister(int iaddress, int count)
        {
            int length = (count / 16) + ((count % 16 > 0) ? 1 : 0);
            List<bool> value = new List<bool>();
            List<ushort> list = ReadInputRegister(iaddress, length);
            for (int i = 0; i < list.Count; i++)
            {
                for (int j = 0; j < 16; j++)
                {
                    value.Add(((list[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        public bool SetCoil(int iaddress, bool bl)
        {
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(bl ? 0xFF : 0x00);
            command[3] = 0x00;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_COIL, command);
            byte[] rtResponse = TryExecution(sdCommand);
            int ctLength = rtResponse.Length;
            byte[] crc = rtResponse.Take(ctLength - 2).ToArray().GetCRC();
            return rtResponse[ctLength - 2] == crc[0] && rtResponse[ctLength - 1] == crc[1];
        }

        public bool SetCoil(int iaddress, bool[] bl)
        {
            byte[] value = new List<bool>(bl).BoolsToByte();
            byte[] command = new byte[5 + value.Length];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(bl.Length >> 8);
            command[3] = (byte)bl.Length;
            command[4] = (byte)value.Length;
            Array.Copy(value, 0, command, 5, value.Length);
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_MULTIPLE_COIL, command);
            byte[] rtResponse = TryExecution(sdCommand);
            int ctLength = rtResponse.Length;
            byte[] crc = rtResponse.Take(ctLength - 2).ToArray().GetCRC();
            return rtResponse[ctLength - 2] == crc[0] && rtResponse[ctLength - 1] == crc[1];
        }

        public List<bool> GetCoil(int iaddress, int count = 0)
        {
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_COIL_STATUS, command);
            byte[] rtResponse = TryExecution(sdCommand);
            List<bool> value = new List<bool>();
            for (int i = 3; i < rtResponse.Length - 2; i++)
            {
                for (int j = 0; j < 8; j++)
                {
                    value.Add(((rtResponse[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        public bool GetCoil(int iaddress) => GetCoil(iaddress, 1)[0];

        public List<bool> GetDiscreteInput(int iaddress, int count = 0)
        {
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_INPUT_STATUS, command);
            byte[] rtResponse = TryExecution(sdCommand);
            List<bool> value = new List<bool>();            
            for (int i = 3; i < rtResponse.Length - 2; i++)
            {
                for (int j = 0; j < 8; j++)
                {
                    value.Add(((rtResponse[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        public bool GetDiscreteInput(int iaddress) => GetDiscreteInput(iaddress, 1)[0];
    }
}
